/**
 * ********************************************************************************
 *
 * Version Date: January 8, 2018
 *
 * Contributor(s):
 *     C. Heazel (WiSC): Applied mofications to address Fortify issues
 *
 * ********************************************************************************
 */

package com.occamlab.te.parsers;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A SAX error handler that collects validation errors raised while verifying
 * the structure and content of XML entities.
 * 
 */
public class XmlErrorHandler implements ErrorHandler {

    /** Storage for error messages. */
    private StringBuffer buf = new StringBuffer();

    /** Collection of reported validation errors. */
    private List<ValidationError> errors = new ArrayList<ValidationError>();
    private static Logger jlogger = Logger
            .getLogger("com.occamlab.te.parsers.XmlErrorHandler");

    /**
     * Indicates whether any validation errors have been reported.
     * 
     * @return true if any validation errors have been received.
     */
    public boolean isEmpty() {
        return errors.isEmpty();
    }

    /**
     * Receive notification of a warning.
     * 
     * @param spex
     *            a non-error condition reported by the parser
     * @see ErrorHandler#warning(SAXParseException)
     */
    public void warning(SAXParseException spex) {
        addError(ValidationError.WARNING, spex);
    }

    /**
     * Receive notification of a recoverable error. Typically this indicates
     * that a validation constraint has been violated.
     *
     * @param spex
     *            a non-fatal error condition reported by the parser
     * @see ErrorHandler#error(SAXParseException)
     */
    public void error(SAXParseException spex) {
        addError(ValidationError.ERROR, spex);
    }

    /**
     * Prints the error to STDOUT, used to be consistent with TEAM Engine error
     * handler.
     *
     */
    void printError(String type, SAXParseException e) {
        PrintWriter logger = new PrintWriter(System.out);
        logger.print(type);
        if (e.getLineNumber() >= 0) {
            logger.print(" at line " + e.getLineNumber());
            if (e.getColumnNumber() >= 0) {
                logger.print(", column " + e.getColumnNumber());
            }
            if (e.getSystemId() != null) {
                logger.print(" of " + e.getSystemId());
            }
        } else {
            if (e.getSystemId() != null) {
                logger.print(" in " + e.getSystemId());
            }
        }
        logger.println(":");
        logger.println("  " + e.getMessage());
        logger.flush();
    }

    /**
     * Receive notification of a non-recoverable error, such as a violation of
     * the well-formedness constraint.
     *
     * @param spex
     *            a fatal error condition reported by the parser
     * @throws SAXException
     *             if a fatal error (e.g., non-XML input, or ill-formed XML)
     *             occurs while parsing the input
     * @see ErrorHandler#fatalError(SAXParseException)
     */
    public void fatalError(SAXParseException spex) throws SAXException {

        addError(ValidationError.FATAL_ERROR, spex);
        throw new SAXException("Fatal error while parsing input.");
    }

    /**
     * Adds a validation error based on a <code>SAXParseException</code>.
     * 
     * @param severity
     *            the severity of the error
     * @param spex
     *            the <code>SAXParseException</code> raised while validating the
     *            XML source
     */
    private void addError(short severity, SAXParseException spex) {
        if (spex.getLineNumber() > 0) {
            buf.append("Line " + spex.getLineNumber() + " - ");
        }
        buf.append(spex.getMessage() + "\n");
        ValidationError error = new ValidationError(severity, buf.toString());
        errors.add(error);
        buf.setLength(0);
    }

    /**
     * Returns a concatenation of all received error messages.
     * 
     * @return a consolidated error message
     */
    public String toString() {
        buf.setLength(0);
        ErrorIterator errIterator = iterator();
        while (errIterator.hasNext()) {
            ValidationError err = errIterator.next();
            buf.append(err.getMessage());
        }
        return buf.toString();
    }

    /**
     * Returns a list of errors as strings.
     * 
     * @return a list of error strings.
     */
    public List<String> toList() {

        List<String> errorStrings = new ArrayList<String>();
        ErrorIterator errIterator = iterator();
        while (errIterator.hasNext()) {
            ValidationError err = errIterator.next();
            errorStrings.add(err.getMessage());
        }
        return errorStrings;
    }

    /**
     * Returns validation errors in a root container node.
     * The root container ({@code <errors>}) contains a list
     * of {@code <error>} elements containing error
     * messages as text content.
     * 
     * @return Root element containing list of errors
     */
    public Element toRootElement() {
        Document doc = null;
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            DocumentBuilder db = dbf.newDocumentBuilder();
            doc = db.newDocument();
        } catch (Exception e) {
            jlogger.log(Level.SEVERE, "validate", e);
            e.printStackTrace();
            // Fortify Mod: if we get here then there is no point in going further
            return null;
        }
        Element root = doc.createElement("errors");
        doc.appendChild(root);
        ErrorIterator errIterator = iterator();
        while (errIterator.hasNext()) {
            ValidationError err = errIterator.next();
            Element elem = doc.createElement("error");
            elem.setTextContent(err.getMessage());
            root.appendChild(elem);
        }
        return root;
    }

    /**
     * Returns validation errors in a NodeList (needed for CTL processing). Each
     * item in the list is an {@code <error>} element containing an error
     * message as text content.
     * 
     * @return a list of errors in a NodeList.
     */
    public NodeList toNodeList() {
        return toRootElement().getElementsByTagName("error");
    }

    /**
     * Returns an iterator over the validation errors collected by this handler.
     * 
     * @return a read-only <code>ErrorIterator</code> for this handler
     */
    public ErrorIterator iterator() {
        return new ErrorIterator();
    }

    /**
     * Clears all errors and messages.
     */
    public void reset() {
        buf.setLength(0);
        errors.clear();
    }

    /**
     * Helper class that provides a read-only iterator over validation errors.
     */
    public class ErrorIterator {
        /** The underlying errors for this iterator. */
        Iterator<ValidationError> underlying = errors.iterator();

        /**
         * Indicates if more errors remain in the iteration.
         * 
         * @return <code>true</code> if more errors remain.
         */
        public boolean hasNext() {
            return underlying.hasNext();
        }

        /**
         * Returns the next validation error in the iteration.
         * 
         * @return the next error
         */
        public ValidationError next() {
            return (ValidationError) underlying.next();
        }
    }
}
